--- title: "Drawing interactive maps with Leaflet" date: 2019-03-01 type: docs draft: false aliases: ["/geoviz_leaflet.html"] categories: ["dataviz", "geospatial"] menu: notes: parent: Geospatial visualization weight: 6 ---
library(tidyverse)
library(leaflet)
library(stringr)
library(sf)
library(here)

options(digits = 3)
set.seed(1234)
theme_set(theme_minimal())

Leaflet is an open-source JavaScript library for creating interactive maps. Unlike static visualization packages such as ggplot2 or ggmap, Leaflet maps are fully interactive and can include features such as:

It is used by many news organizations and tech websites to visualize geographic data. The leaflet package for R enables the creation of interactive maps within R without learning how to write JavaScript code. The leaflet documentation is a handy walkthrough for the basics of creating Leaflet maps in R. Let’s explore here how to create Leaflet maps using the same data we used to create raster maps with ggmap, crime data from the city of Chicago in 2017.1

crimes <- here("static", "data", "Crimes_-_2017.csv") %>%
  read_csv()
glimpse(crimes)
## Observations: 267,345
## Variables: 22
## $ ID                     <dbl> 11094370, 11118031, 11134189, 11156462, 1…
## $ `Case Number`          <chr> "JA440032", "JA470589", "JA491697", "JA52…
## $ Date                   <chr> "09/21/2017 12:15:00 AM", "10/12/2017 07:…
## $ Block                  <chr> "072XX N CALIFORNIA AVE", "055XX W GRAND …
## $ IUCR                   <chr> "1122", "1345", "4651", "1110", "0265", "…
## $ `Primary Type`         <chr> "DECEPTIVE PRACTICE", "CRIMINAL DAMAGE", …
## $ Description            <chr> "COUNTERFEIT CHECK", "TO CITY OF CHICAGO …
## $ `Location Description` <chr> "CURRENCY EXCHANGE", "JAIL / LOCK-UP FACI…
## $ Arrest                 <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE,…
## $ Domestic               <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,…
## $ Beat                   <chr> "2411", "2515", "0922", "2514", "1221", "…
## $ District               <chr> "024", "025", "009", "025", "012", "002",…
## $ Ward                   <dbl> 50, 29, 12, 30, 32, 20, 9, 12, 12, 27, 32…
## $ `Community Area`       <dbl> 2, 19, 58, 19, 24, 40, 49, 30, 30, 23, 24…
## $ `FBI Code`             <chr> "10", "14", "26", "11", "02", "15", "03",…
## $ `X Coordinate`         <dbl> 1156443, 1138788, 1159425, 1138653, 11612…
## $ `Y Coordinate`         <dbl> 1947707, 1913480, 1875711, 1920720, 19052…
## $ Year                   <dbl> 2017, 2017, 2017, 2017, 2017, 2017, 2017,…
## $ `Updated On`           <chr> "03/01/2018 03:52:35 PM", "03/01/2018 03:…
## $ Latitude               <dbl> 42.0, 41.9, 41.8, 41.9, 41.9, 41.8, 41.7,…
## $ Longitude              <dbl> -87.7, -87.8, -87.7, -87.8, -87.7, -87.6,…
## $ Location               <chr> "(42.012293397, -87.699714109)", "(41.918…

0.1 Basic usage

Leaflet maps are built using layers, similar to ggplot2.

  1. Create a map widget by calling leaflet()
  2. Add layers to the map using one or more of the layer functions (e.g. addTiles(), addMarkers(), addPolygons())
  3. Repeat step 2 as many times as necessary to incorporate the necessary information
  4. Display the map widget

A basic example is:

m <- leaflet() %>%
  addTiles() %>%
  addMarkers(lng = -87.597241, lat = 41.789829,
             popup = "Saieh Hall of Economics")
m

0.2 Basemaps

Like ggmap, leaflet supports basemaps using map tiles. By default, OpenStreetMap tiles are used.

m <- leaflet() %>%
  setView(lng = -87.618994, lat = 41.875619, zoom = 12)
m %>% addTiles()

To use a different basemap provider, use addProviderTiles() with the name of the provider you wish to implement. You can view the complete list of providers using names(providers).

m %>% addProviderTiles(providers$Stamen.Toner)
m %>% addProviderTiles(providers$CartoDB.Positron)
m %>% addProviderTiles(providers$Esri.NatGeoWorldMap)
m %>% addProviderTiles(providers$Wikimedia)

0.3 Add markers

Markers are used to identify points on the map. Each point needs to be defined in terms of latitude/longitude coordinates. These can come from a variety of sources, most commonly either a map data file such as a shapefile or GeoJSON (imported using sf) or a data frame with latitude and longitude columns.

Let’s use the Chicago crimes data to draw a map of the city identifying the location of each reported homicide:

(homicides <- crimes %>%
  filter(`Primary Type` == "HOMICIDE"))
## # A tibble: 671 x 22
##        ID `Case Number` Date  Block IUCR  `Primary Type` Description
##     <dbl> <chr>         <chr> <chr> <chr> <chr>          <chr>      
##  1 2.31e4 JA149608      02/1… 001X… 0110  HOMICIDE       FIRST DEGR…
##  2 2.39e4 JA530946      11/3… 088X… 0110  HOMICIDE       FIRST DEGR…
##  3 2.34e4 JA302423      06/1… 047X… 0110  HOMICIDE       FIRST DEGR…
##  4 2.34e4 JA312425      06/1… 006X… 0110  HOMICIDE       FIRST DEGR…
##  5 2.37e4 JA490016      10/2… 048X… 0110  HOMICIDE       FIRST DEGR…
##  6 2.32e4 JA210752      04/0… 013X… 0110  HOMICIDE       FIRST DEGR…
##  7 2.36e4 JA461918      10/0… 018X… 0110  HOMICIDE       FIRST DEGR…
##  8 2.36e4 JA461918      10/0… 018X… 0110  HOMICIDE       FIRST DEGR…
##  9 1.08e7 JA138326      02/0… 013X… 0142  HOMICIDE       RECKLESS H…
## 10 2.35e4 JA364517      07/2… 047X… 0110  HOMICIDE       FIRST DEGR…
## # … with 661 more rows, and 15 more variables: `Location
## #   Description` <chr>, Arrest <lgl>, Domestic <lgl>, Beat <chr>,
## #   District <chr>, Ward <dbl>, `Community Area` <dbl>, `FBI Code` <chr>,
## #   `X Coordinate` <dbl>, `Y Coordinate` <dbl>, Year <dbl>, `Updated
## #   On` <chr>, Latitude <dbl>, Longitude <dbl>, Location <chr>
leaflet(data = homicides) %>%
  addTiles() %>%
  addMarkers()

addMarkers() and related functions will automatically check data frames for columns called lng/long/longitude and lat/latitude (case-insensitively). If your coordinate columns have any other names, you need to explicitly identify them using the lng and lat arguments. Such as `addMarkers(lng = ~Longitude, lat = ~Latitude).

Without any customization, we get a basic map with each murder location indicated by a dropped pin. Each markers appearance can be customized, though the technical difficulty quickly ramps up. The awesome markers plugin offers the most straight-forward customizability options. Instead of using addMarkers(), use addAwesomeMarkers() to control the appearance of the markers using icons from the Font Awesome, Bootstrap Glyphicons, and Ion icons icon libraries. First you define the appearance of the icon using awesomeIcons(), then pass that as an argument to addAwesomeMarkers():

icons <- awesomeIcons(
  icon = 'bolt',
  iconColor = 'orange',
  markerColor = "black",
  library = 'fa'
)

leaflet(data = homicides) %>%
  addTiles() %>%
  addAwesomeMarkers(icon = icons)

One concern is that some neighborhoods have so many murders that the points overlap. One solution enabled by Leaflet’s interactivity is to cluster markers at varying levels of detail using the clusterOptions argument to addMarkers():

leaflet(data = homicides) %>%
  addTiles() %>%
  addMarkers(clusterOptions = markerClusterOptions())

Alternatively, we could use circles using addCircleMarkers():

leaflet(data = homicides) %>%
  addTiles() %>%
  addCircleMarkers()

0.4 Add labels and popups

Each point can have text added to it using either a label (appears either on hover or statically) or a popup (appears only on click). For instance we can label each murder with the date/timestamp when it was originally reported.

leaflet(data = homicides) %>%
  addTiles() %>%
  addMarkers(label = ~Date)

If we only want the information to appear when we click on the point, we should instead use popup = ~Date:

leaflet(data = homicides) %>%
  addTiles() %>%
  addMarkers(popup = ~Date)

We can combine multiple pieces of information to create a custom popup message. Unfortunately this does require basic knowledge of writing HTML documents.

homicides %>%
  mutate(popup = str_c(Date,
                       Block,
                       str_c("Location type:", `Location Description`,
                             sep = " "),
                       sep = "<br/>")) %>%
  leaflet() %>%
  addTiles() %>%
  addMarkers(popup = ~popup)

0.5 Add lines and shapes

Leaflet can also draw spatial lines and shapes from R and add them to maps. Given our previous exposure to sf and importing shapefiles using st_read(), let draw a map of Chicago with each community area outlined.

areas <- here("static", "data",
              "Boundaries - Community Areas (current)",
              "geo_export_328cdcbf-33ba-4997-8ce8-90953c6fec19.shp") %>%
  st_read() %>%
  # convert community names to title case
  mutate(community = str_to_title(community))
## Reading layer `geo_export_328cdcbf-33ba-4997-8ce8-90953c6fec19' from data source `/Users/soltoffbc/Projects/Computing for Social Sciences/course-site/static/data/Boundaries - Community Areas (current)/geo_export_328cdcbf-33ba-4997-8ce8-90953c6fec19.shp' using driver `ESRI Shapefile'
## Simple feature collection with 77 features and 9 fields
## geometry type:  MULTIPOLYGON
## dimension:      XY
## bbox:           xmin: -87.9 ymin: 41.6 xmax: -87.5 ymax: 42
## epsg (SRID):    4326
## proj4string:    +proj=longlat +ellps=WGS84 +no_defs

To do this in ggplot(), we only need two lines of code:

ggplot(data = areas) +
  geom_sf()

To draw this in leaflet, we use addPolygons():

leaflet(data = areas) %>%
  addPolygons(color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE))

The first several arguments adjust the appearance of each polygon region (e.g. color, opacity, border thickness). highlightOptions emphasizes the currently moused-over polygon. We can further add detail to this map by labeling each community area just as we did with points:

leaflet(data = areas) %>%
  addPolygons(label = ~community,
              color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE))

And since leaflet map widgets are built in layers, we can overlay the community areas on top of a standard map of the city.

leaflet(data = areas) %>%
  addTiles() %>%
  addPolygons(label = ~community,
              color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE))

0.6 Choropleth of homicides by neighborhood

Now that we have a basic map of the city of Chicago with each community area identified, we can turn this map into a choropleth by filling in the color of each community area based on the number of reported homicides in 2017. First we calculate the total number of reported homicides by community area and merge this with the simple features data frame:

(areas_homicides <- areas %>%
   select(community, area_numbe) %>%
   mutate(area_numbe = as.numeric(as.character(area_numbe))) %>%
   left_join(homicides %>%
               count(`Community Area`),
             by = c("area_numbe" = "Community Area")) %>%
   mutate(n = ifelse(is.na(n), 0, n)))
## Simple feature collection with 77 features and 3 fields
## geometry type:  MULTIPOLYGON
## dimension:      XY
## bbox:           xmin: -87.9 ymin: 41.6 xmax: -87.5 ymax: 42
## epsg (SRID):    4326
## proj4string:    +proj=longlat +ellps=WGS84 +no_defs
## First 10 features:
##          community area_numbe  n                       geometry
## 1          Douglas         35  3 MULTIPOLYGON (((-87.6 41.8,...
## 2          Oakland         36  0 MULTIPOLYGON (((-87.6 41.8,...
## 3      Fuller Park         37  2 MULTIPOLYGON (((-87.6 41.8,...
## 4  Grand Boulevard         38 10 MULTIPOLYGON (((-87.6 41.8,...
## 5          Kenwood         39  4 MULTIPOLYGON (((-87.6 41.8,...
## 6   Lincoln Square          4  1 MULTIPOLYGON (((-87.7 42, -...
## 7  Washington Park         40 12 MULTIPOLYGON (((-87.6 41.8,...
## 8        Hyde Park         41  1 MULTIPOLYGON (((-87.6 41.8,...
## 9         Woodlawn         42 14 MULTIPOLYGON (((-87.6 41.8,...
## 10     Rogers Park          1  4 MULTIPOLYGON (((-87.7 42, -...

0.7 Add color

Next we need to define the color palette for this map. leaflet has its own series of functions to generate palettes using either RColorBrewer or viridis.

First, we define the bins. This is a numeric vector that defines the boundaries between intervals ((0,10], (10,20], and so on).

Then, we call colorBin() to generate a palette function that maps the RColorBrewer "YlOrRd" palette to our bins.

Finally, we modify addPolygons() to use the palette function and the density values to generate a vector of colors for fillColor(), and also add some other static style properties.

bins <- c(0, 10, 20, 30, 40, 50, Inf)
pal <- colorBin("YlOrRd", domain = areas_homicides$n, bins = bins)

areas_homicides %>%
  leaflet() %>%
  addTiles() %>%
  addPolygons(label = ~community,
              fillColor = ~pal(n),
              color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE))

0.8 Custom information

Let’s now modify the label to explicitly identify the number of reported homicides in the community area. We generate the HTML by hand and pass it to map(htmltools::HTML) so that Leaflet knows to treat each label as HTML instead of plain text. We also adjust the appearance of each label using the labelOptions argument and corresponding function.

areas_homicides %>%
  mutate(popup = str_c("<strong>", community, "</strong>",
                       "<br/>",
                       "Reported homicides in 2017: ", n) %>%
           map(htmltools::HTML)) %>%
  leaflet() %>%
  addTiles() %>%
  addPolygons(label = ~popup,
              fillColor = ~pal(n),
              color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE),
              labelOptions = labelOptions(
                style = list("font-weight" = "normal", padding = "3px 8px"),
                textsize = "15px",
                direction = "auto"))

0.9 Add legend

Finally, we add a legend using addLegend().

areas_homicides %>%
  mutate(popup = str_c("<strong>", community, "</strong>",
                       "<br/>",
                       "Reported homicides in 2017: ", n) %>%
           map(htmltools::HTML)) %>%
  leaflet() %>%
  addTiles() %>%
  addPolygons(label = ~popup,
              fillColor = ~pal(n),
              color = "#444444",
              weight = 1,
              smoothFactor = 0.5,
              opacity = 1.0,
              fillOpacity = 0.5,
              highlightOptions = highlightOptions(color = "white",
                                                  weight = 2,
                                                  bringToFront = TRUE),
              labelOptions = labelOptions(
                style = list("font-weight" = "normal", padding = "3px 8px"),
                textsize = "15px",
                direction = "auto")) %>%
  addLegend(pal = pal,
            values = ~n,
            opacity = 0.7,
            title = NULL,
            position = "bottomright")

The main requirement here is pal = pal, which tells addLegend() the custom palette function used to create the color palette.

0.10 Session Info

devtools::session_info()
## ─ Session info ──────────────────────────────────────────────────────────
##  setting  value                       
##  version  R version 3.5.3 (2019-03-11)
##  os       macOS Mojave 10.14.3        
##  system   x86_64, darwin15.6.0        
##  ui       X11                         
##  language (EN)                        
##  collate  en_US.UTF-8                 
##  ctype    en_US.UTF-8                 
##  tz       America/Chicago             
##  date     2019-03-28                  
## 
## ─ Packages ──────────────────────────────────────────────────────────────
##  package      * version date       lib source        
##  assertthat     0.2.1   2019-03-21 [2] CRAN (R 3.5.3)
##  backports      1.1.3   2018-12-14 [2] CRAN (R 3.5.0)
##  blogdown       0.11    2019-03-11 [1] CRAN (R 3.5.2)
##  bookdown       0.9     2018-12-21 [1] CRAN (R 3.5.0)
##  broom          0.5.1   2018-12-05 [2] CRAN (R 3.5.0)
##  callr          3.2.0   2019-03-15 [2] CRAN (R 3.5.2)
##  cellranger     1.1.0   2016-07-27 [2] CRAN (R 3.5.0)
##  class          7.3-15  2019-01-01 [2] CRAN (R 3.5.3)
##  classInt       0.3-1   2018-12-18 [2] CRAN (R 3.5.0)
##  cli            1.1.0   2019-03-19 [1] CRAN (R 3.5.2)
##  colorspace     1.4-1   2019-03-18 [2] CRAN (R 3.5.2)
##  crayon         1.3.4   2017-09-16 [2] CRAN (R 3.5.0)
##  crosstalk      1.0.0   2016-12-21 [2] CRAN (R 3.5.0)
##  DBI            1.0.0   2018-05-02 [2] CRAN (R 3.5.0)
##  desc           1.2.0   2018-05-01 [2] CRAN (R 3.5.0)
##  devtools       2.0.1   2018-10-26 [1] CRAN (R 3.5.1)
##  digest         0.6.18  2018-10-10 [1] CRAN (R 3.5.0)
##  dplyr        * 0.8.0.1 2019-02-15 [1] CRAN (R 3.5.2)
##  e1071          1.7-1   2019-03-19 [1] CRAN (R 3.5.2)
##  evaluate       0.13    2019-02-12 [2] CRAN (R 3.5.2)
##  fansi          0.4.0   2018-10-05 [2] CRAN (R 3.5.0)
##  forcats      * 0.4.0   2019-02-17 [2] CRAN (R 3.5.2)
##  fs             1.2.7   2019-03-19 [1] CRAN (R 3.5.3)
##  generics       0.0.2   2018-11-29 [1] CRAN (R 3.5.0)
##  ggplot2      * 3.1.0   2018-10-25 [1] CRAN (R 3.5.0)
##  glue           1.3.1   2019-03-12 [2] CRAN (R 3.5.2)
##  gtable         0.2.0   2016-02-26 [2] CRAN (R 3.5.0)
##  haven          2.1.0   2019-02-19 [2] CRAN (R 3.5.2)
##  here         * 0.1     2017-05-28 [2] CRAN (R 3.5.0)
##  hms            0.4.2   2018-03-10 [2] CRAN (R 3.5.0)
##  htmltools      0.3.6   2017-04-28 [1] CRAN (R 3.5.0)
##  htmlwidgets    1.3     2018-09-30 [2] CRAN (R 3.5.0)
##  httpuv         1.5.0   2019-03-15 [2] CRAN (R 3.5.2)
##  httr           1.4.0   2018-12-11 [2] CRAN (R 3.5.0)
##  jsonlite       1.6     2018-12-07 [2] CRAN (R 3.5.0)
##  knitr          1.22    2019-03-08 [2] CRAN (R 3.5.2)
##  later          0.8.0   2019-02-11 [2] CRAN (R 3.5.2)
##  lattice        0.20-38 2018-11-04 [2] CRAN (R 3.5.3)
##  lazyeval       0.2.2   2019-03-15 [2] CRAN (R 3.5.2)
##  leaflet      * 2.0.2   2018-08-27 [1] CRAN (R 3.5.0)
##  lubridate      1.7.4   2018-04-11 [2] CRAN (R 3.5.0)
##  magrittr       1.5     2014-11-22 [2] CRAN (R 3.5.0)
##  memoise        1.1.0   2017-04-21 [2] CRAN (R 3.5.0)
##  mime           0.6     2018-10-05 [1] CRAN (R 3.5.0)
##  modelr         0.1.4   2019-02-18 [2] CRAN (R 3.5.2)
##  munsell        0.5.0   2018-06-12 [2] CRAN (R 3.5.0)
##  nlme           3.1-137 2018-04-07 [2] CRAN (R 3.5.3)
##  pillar         1.3.1   2018-12-15 [2] CRAN (R 3.5.0)
##  pkgbuild       1.0.3   2019-03-20 [1] CRAN (R 3.5.3)
##  pkgconfig      2.0.2   2018-08-16 [2] CRAN (R 3.5.1)
##  pkgload        1.0.2   2018-10-29 [1] CRAN (R 3.5.0)
##  plyr           1.8.4   2016-06-08 [2] CRAN (R 3.5.0)
##  prettyunits    1.0.2   2015-07-13 [2] CRAN (R 3.5.0)
##  processx       3.3.0   2019-03-10 [2] CRAN (R 3.5.2)
##  promises       1.0.1   2018-04-13 [2] CRAN (R 3.5.0)
##  ps             1.3.0   2018-12-21 [2] CRAN (R 3.5.0)
##  purrr        * 0.3.2   2019-03-15 [2] CRAN (R 3.5.2)
##  R6             2.4.0   2019-02-14 [1] CRAN (R 3.5.2)
##  RColorBrewer   1.1-2   2014-12-07 [2] CRAN (R 3.5.0)
##  Rcpp           1.0.1   2019-03-17 [1] CRAN (R 3.5.2)
##  readr        * 1.3.1   2018-12-21 [2] CRAN (R 3.5.0)
##  readxl         1.3.1   2019-03-13 [2] CRAN (R 3.5.2)
##  remotes        2.0.2   2018-10-30 [1] CRAN (R 3.5.0)
##  rlang          0.3.2   2019-03-21 [1] CRAN (R 3.5.3)
##  rmarkdown      1.12    2019-03-14 [1] CRAN (R 3.5.2)
##  rprojroot      1.3-2   2018-01-03 [2] CRAN (R 3.5.0)
##  rstudioapi     0.10    2019-03-19 [1] CRAN (R 3.5.3)
##  rvest          0.3.2   2016-06-17 [2] CRAN (R 3.5.0)
##  scales         1.0.0   2018-08-09 [1] CRAN (R 3.5.0)
##  sessioninfo    1.1.1   2018-11-05 [1] CRAN (R 3.5.0)
##  sf           * 0.7-3   2019-02-21 [1] CRAN (R 3.5.2)
##  shiny          1.2.0   2018-11-02 [2] CRAN (R 3.5.0)
##  stringi        1.4.3   2019-03-12 [1] CRAN (R 3.5.2)
##  stringr      * 1.4.0   2019-02-10 [1] CRAN (R 3.5.2)
##  testthat       2.0.1   2018-10-13 [2] CRAN (R 3.5.0)
##  tibble       * 2.1.1   2019-03-16 [2] CRAN (R 3.5.2)
##  tidyr        * 0.8.3   2019-03-01 [1] CRAN (R 3.5.2)
##  tidyselect     0.2.5   2018-10-11 [1] CRAN (R 3.5.0)
##  tidyverse    * 1.2.1   2017-11-14 [2] CRAN (R 3.5.0)
##  units          0.6-2   2018-12-05 [1] CRAN (R 3.5.0)
##  usethis        1.4.0   2018-08-14 [1] CRAN (R 3.5.0)
##  utf8           1.1.4   2018-05-24 [2] CRAN (R 3.5.0)
##  withr          2.1.2   2018-03-15 [2] CRAN (R 3.5.0)
##  xfun           0.5     2019-02-20 [1] CRAN (R 3.5.2)
##  xml2           1.2.0   2018-01-24 [2] CRAN (R 3.5.0)
##  xtable         1.8-3   2018-08-29 [2] CRAN (R 3.5.0)
##  yaml           2.2.0   2018-07-25 [2] CRAN (R 3.5.0)
## 
## [1] /Users/soltoffbc/Library/R/3.5/library
## [2] /Library/Frameworks/R.framework/Versions/3.5/Resources/library

  1. Full documentation of the data from the larger 2001-present crime dataset..